using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using Wrath.Xml;

namespace Wrath.CodeBox
{
    public partial class CodeBox : ContainerControl
    {
        public enum LineContext
        {
            Unknown = 0,
            Comment,
            String,
            Whitespace
        }

        private class LineInfo
        {
            public int Depth = -1;
            public LineContext Context = LineContext.Unknown;
            public int Outdent = 0;
            public bool Changed = false;
        }
        
        public LanguageParser Parser;
        public ColorWay Colors = new ColorWay();

        public new event KeyEventHandler KeyDown;
        public event EventHandler SelectionChanged;
        public new event EventHandler TextChanged;
        public new event EventHandler GotFocus;
        public new event EventHandler LostFocus;

        private RichTextBox InnerBox = new RichTextBox();
        private Font BaseFont = new Font("Courier New", 10);
        private Selection Previous = new Selection();
        private Selection Next = new Selection();

        private LineInfo[] Info = new LineInfo[0];
        private bool isRecoloring = false;
        private bool isPaused = false;
        private bool autoIndent = true;
        private bool redentPending = false;


        public CodeBox()
        {
            base.GotFocus += new EventHandler(CodeBox_GotFocus);
            base.LostFocus += new EventHandler(CodeBox_LostFocus);

            // add the embedded RichTextBox to my controls collection
            Controls.Add(InnerBox);

            // set the position (relative to the wrapper) and the behaviours
            InnerBox.Location = new Point(0, 0);
            InnerBox.Dock = DockStyle.Fill;
            InnerBox.WordWrap = false;
        
            // set the default font
            InnerBox.Font = BaseFont;

            // activate the inner box
            ActiveControl = InnerBox;            
        }

        private void CodeBox_GotFocus(object sender, EventArgs e)
        {
            InnerBox.Select();
            if (GotFocus != null)
            {
                GotFocus(sender, e);
            }
        }

        private void CodeBox_LostFocus(object sender, EventArgs e)
        {
            if (LostFocus != null)
            {
                LostFocus(sender, e);
            }
        }

        public bool AutoIndent
        {
            get { return autoIndent; }
            set { autoIndent = value; }
        }

        public BorderStyle BorderStyle
        {
            get { return InnerBox.BorderStyle; }
            set { InnerBox.BorderStyle = value; }
        }

        public new Cursor Cursor
        {
            get { return InnerBox.Cursor; }
            set { InnerBox.Cursor = value; }
        }

        public bool DetectUrls
        {
            get { return InnerBox.DetectUrls; }
            set { InnerBox.DetectUrls = value; }
        }

        public new Font Font
        {
            get { return BaseFont; }
            set 
            { 
                BaseFont = new Font(value, FontStyle.Regular);
                InnerBox.Font = BaseFont;
            }
        }

        protected bool Paused
        {
            get { return isPaused; }
            set 
            { 
                isPaused = value;
                if (value)
                {
                    // set the mouse cursor
                    InnerBox.Cursor = Cursors.WaitCursor;
        
                    // disable redrawing
                    SendMessage(InnerBox.Handle, WM_SETREDRAW, false, 0);

                    // make the event happen
                    Application.DoEvents();
                }
                else
                {
                    // enable redrawing
                    SendMessage(InnerBox.Handle, WM_SETREDRAW, true, 0);
                    InnerBox.Refresh();

                    // make the event happen
                    Application.DoEvents();

                    // reset the mouse cursor
                    InnerBox.Cursor = Cursors.IBeam;
                }
            }
        }

        public RichTextBoxScrollBars ScrollBars
        {
            get { return InnerBox.ScrollBars; }
            set { InnerBox.ScrollBars = value; }
        }

        public new void Select()
        {
            InnerBox.Select();
        }

        public void Select(int start, int length)
        {
            InnerBox.Select(start, length);
        }

        public new string Text
        {
            get { return InnerBox.Text; }
            set 
            {
                // change the innerbox text (need to disable auto-recolouring first)
                isRecoloring = true;
                InnerBox.Text = value;
                InnerBox.Select(0, 0);
                isRecoloring = false;

                // create new arrays for the line context and nesting depth
                Array.Resize<LineInfo>(ref Info, InnerBox.Lines.Length - 1);

                for (int i = 0; i < Info.Length; i++)
                {
                    Info[i] = new LineInfo();
                }

                // recolour the codebox, from the start
                RecolorLines(0);
                
                // reset the indentation
                Redent(0);
            }
        }

        public bool WordWrap
        {
            get { return false; }
        }

  //' ~~~~ Internal support functions

        private void RecolorLines(int StartLine)
        {
            RecolorLines(StartLine, -1);
        }
            
        private void RecolorLines(int StartLine, int EndLine)
        {
            int posn;
            int start;
            int length;
            int line;
            LineContext ctxt = LineContext.Whitespace;
            LineContext oldctxt = LineContext.Unknown;
            int dpth = 0;
            int topline = 0;
            bool redeep = false;

            // check for trivial case
            if (InnerBox.Lines.Length == 0) return;
            
            // check we have a parser
            if (Parser == null) return;

            // check for optional value
            if (EndLine < 0)
            {
                EndLine = InnerBox.Lines.Length - 1;
            }

            // check for errors
            if (StartLine < 0 || EndLine < StartLine || EndLine >= InnerBox.Lines.Length)
            {
                throw new Exception("Range outside scope");
            }
    
            // find the nearest previous line with a whitespace context
            while (StartLine > -1 && Info[StartLine].Context == LineContext.Whitespace)
            {
                StartLine -= 1;
            }

            // save the current top line in the window
            topline = InnerBox.GetLineFromCharIndex(InnerBox.GetCharIndexFromPosition(new Point(0, 0)));

            // set the starting line and position
            line = StartLine;

            posn = InnerBox.GetFirstCharIndexFromLine(line);
            if (Info[line].Depth > 0)
            {
                dpth = Info[line].Depth;
            }

            // save the current selection
            start = InnerBox.SelectionStart;
            length = InnerBox.SelectionLength;

            // disable SelectionChanged and TextChanged events
            isRecoloring = true;

            // fetch the parser source
            Parser.Source = InnerBox.Text.Substring(posn + 1);

            // disable screen updating
            Paused = true;

            // break the script into tokens
            while (!Parser.IsFinished && (line > EndLine && oldctxt == ctxt && !redeep))
            {
                Token tok = Parser.LexNext();

                // is this an outdenting token?
                if (Parser.Language.Outdents.ContainsKey(tok.Value))
                {
                    // yes, decrease depth
                    dpth -= 1;
        
                    // check for underflow
                    if (dpth < 0) 
                    {
                        dpth = 0;
                    }
        
                    // change the current line outdenting
                    Info[line].Outdent = 1;
                }

                // is this an indenting token?
                if (Parser.Language.Indents.ContainsKey(tok.Value))
                {                    
                    // yes, increase depth
                    dpth += 1;
                }

                // have we advanced any line(s)?
                if (tok.Value.IndexOf("\n") > 0)
                {
                    string str = tok.Value;
                    int i = str.IndexOf("\n");
        
                    // reset the depth-change flag
                    redeep = false;
        
                    // which token type?
                    switch(tok.Type)
                    {
                        case "comment": ctxt = LineContext.Comment; break;
                        case "whitespace": ctxt = LineContext.Whitespace; break;
                        case "string": ctxt = LineContext.String; break;
                        default : ctxt = LineContext.Unknown; break;
                    }

                    while (i > 0)
                    {
                        line += 1;
                        oldctxt = Info[line].Context;
                        if (ctxt != oldctxt)
                        {
                            Info[line].Context = ctxt;
                        }
          
                        // set the line indentation
                        if (Info[line].Depth != dpth)
                        {
                            redeep = true;
                            Info[line].Depth = dpth;
                            Info[line].Changed = true;
                        }
          
                        // reset the next line's outdenting
                        Info[line].Outdent = 0;
                        i = str.IndexOf("\n", i + 1);
                    }
                }

                InnerBox.Select(posn, tok.Value.Length);
      
                // set the color and style
                try
                {
                    InnerBox.SelectionColor = Colors[tok.Type];
                    InnerBox.SelectionFont = BaseFont;
                }
                catch
                {
                    InnerBox.SelectionColor = Colors["error"];
                    InnerBox.SelectionFont = new System.Drawing.Font(BaseFont, FontStyle.Underline);
                }

                // re-write the token value (forces preferred case, if needed)
                InnerBox.SelectedText = tok.Value;
      
                // move to the next token
                posn += tok.Value.Length;
      
                // whitespace resumes at the end of the token
                ctxt = LineContext.Whitespace;
            }
    
            // flush the event buffer
            Application.DoEvents();

            // re-enable screen updating
            Paused = false;
    
            // reposition the content within the window
            InnerBox.Select(InnerBox.GetFirstCharIndexFromLine(topline), 0);
            InnerBox.Focus();
    
            // restore the selection
            InnerBox.Select(start, length);
    
            // re-enable SelectionChanged and TextChanged events
            isRecoloring = false;
        }

        private void Redent(int StartLine)
        {
            Redent(StartLine, -1);
        }
        
        private void Redent(int StartLine, int EndLine)
        {
            int line;
            int start;
            int length;
            int delta = 0;
            int topline = 0;

            // check for trivial case
            if (InnerBox.Lines.Length == 0) return;

            // do nothing if not autoindenting
            if (!autoIndent) return;           

            // check for optional value
            if (EndLine < 0)
            {
                EndLine = InnerBox.Lines.Length - 1;
            }

            // check for errors
            if (StartLine < 0 || EndLine < StartLine || EndLine >= InnerBox.Lines.Length)
            {
                throw new Exception("Range outside scope");
            }
   
            // save the current selection
            start = InnerBox.SelectionStart;
            length = InnerBox.SelectionLength;

            // save the current top line in the window
            topline = InnerBox.GetLineFromCharIndex(InnerBox.GetCharIndexFromPosition(new Point(0, 0)));
    
            // set the starting line and position
            line = StartLine;
    
            // disable SelectionChanged and TextChanged events
            isRecoloring = true;
    
            // disable screen updating
            Paused = true;
    
            // break the script into lines
            do 
            {
      
                // don't do anything unless the line starts with whitespace
                if (Info[line].Context != LineContext.Whitespace)
                {
                    delta = 0;
                }
                else
                {
                    int posn = InnerBox.GetFirstCharIndexFromLine(line);
                    int nxtpos;
                    string fill = "";
                    string oldtext;
                    string reptext;
                    int net;
        
                    // get the starting position of this line
                    if (posn < 0)
                    { 
                        posn = 0;
        
                        // create the leading space filler
                        net = Info[line].Depth - Info[line].Outdent;
        
                        if (net > 0)
                        {
                            fill = fill.PadLeft(2 * net);
                        }
                        else
                        {
                            fill = "";
                        }
        
                        // find the line text
                        oldtext = InnerBox.Lines[line];
        
                        // set the drop-through values
                        reptext = oldtext;
                        nxtpos = posn + oldtext.Length;

                        // remove leading tabs and spaces
                        //For i As Integer = 1 To Len(oldtext)
                        //  if Mid(oldtext, i, 1) <> " " AndAlso Mid(oldtext, i, 1) <> vbTab {
                        //    ' finished trimming
                        //    reptext = Microsoft.VisualBasic.Left(oldtext, i - 1)
                        //    nxtpos = posn + i - 1
                        //    Exit For
                        //  }
                        //}
                        reptext = reptext.TrimStart(' ', '\t');

                        // get the difference in line lengths
                        delta = fill.Length - reptext.Length;

                        // if there's any change, replace the innerbox text source
                        if (reptext != fill)
                        {
                            InnerBox.Select(posn, nxtpos - posn);
                            InnerBox.SelectedText = fill;
          
                            // update the start and selection, if we have inserted space on the line
                            if (start >= posn && start <= nxtpos)
                            {
                                start += delta;
                            }
                        }
        
                        // clear the Changed flag
                        Info[line].Changed = false;
                    }
                }
      
                // move to the next line
                line += 1;

            } while (line == Info.Length || (line > EndLine && delta == 0));

            // flush the event buffer
            Application.DoEvents();

            // re-enable screen updating
            Paused = false;

            // reposition the content within the window
            InnerBox.Select(InnerBox.GetFirstCharIndexFromLine(topline), 0);
            InnerBox.Focus();

            // restore the selection
            InnerBox.Select(start, length);

            // re-enable SelectionChanged and TextChanged events
            isRecoloring = false;
    
            // done
        }

        private void InsertLines(int Start, int Count)
        {
            // inserts blank entries into the Info array
            if (Count == 0) return;
            if (Count < 0)
            {
                RemoveLines(Start, -Count);
                return;
            }

            if (Start < 0 || Start > Info.Length)
            {
                throw new Exception("Cannot insert entries outside of the array");
            }
    
            // re-size the array
            Array.Resize<LineInfo>(ref Info, Info.Length + Count - 1);
    
            // copy the entries
            Array.Copy(Info, Start, Info, Start + Count, Info.Length - Start - Count);
    
            // create new entries for the inserted lines
            for (int i = Start; i < Start + Count - 1; i++)
            {
                Info[i] = new LineInfo();
            }
        }

        private void RemoveLines(int Start, int Count)
        {
            // removes entries from the Info array
            if (Count == 0) return;
            if (Count < 0)
            {
                InsertLines(Start, -Count);
                return;
            }
            
            if (Start < 0 || Start + Count > Info.Length)
            {
                throw new Exception("Cannot remove entries outside of the array");
            }
    
            // copy the lines
            Array.Copy(Info, Start + Count, Info, Start, Info.Length - Start - Count);

            // re-size the array
            Array.Resize<LineInfo>(ref Info, Info.Length - Count - 1);
        }

        private const int WM_SETREDRAW = 0x0B;

        private int SendMessage(IntPtr hWnd, int wMsg, bool wParam, int lParam)
        {
            IntPtr wP = new IntPtr(Convert.ToInt32(wParam));
            IntPtr lP = new IntPtr(lParam);

            Message m = Message.Create(hWnd, wMsg, wP, lP);

            base.DefWndProc(ref m);

            return 0;
        }

  
        // ~~~~ Event handlers

        private void InnerBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Z && e.Control)
            {
                // disable Undo
                e.SuppressKeyPress = true;
                e.Handled = true;
            }

            // propogate the event
            if (KeyDown != null) KeyDown(sender, e);
        }

        private void InnerBox_SelectionChanged(object sender, EventArgs e)
        {
            if (!isRecoloring)
            {
                // save previous values
                Previous.Value = Next.Value;
      
                // save new values for next time            
                Next.Start = InnerBox.SelectionStart;
                Next.Length = InnerBox.SelectionLength;
                Next.StartLine = InnerBox.GetLineFromCharIndex(Next.Start);
                Next.EndLine = InnerBox.GetLineFromCharIndex(Next.Start + Next.Length);

                // do we need to re-indent?
                if ((Previous.StartLine != Next.StartLine) || (Next.StartLine != Next.EndLine)) 
                {
                    redentPending = true;
                } else {
                    redentPending = false;
                }
            }
      
            // propogate the event
            if (SelectionChanged != null) SelectionChanged(sender, e);
        }

        private void InnerBox_TextChanged(object sender, EventArgs e)
        {
            if (!isRecoloring)
            {
      
                // recolouring the entire code box is too slow, so need to focus on changed text
                int linecount = InnerBox.Lines.Length - Info.Length;

                if (Previous.Length > 0)
                {
            
                    // previous was a selection, which is deleted
                    if (Previous.EndLine > Previous.StartLine)
                    {
                        RemoveLines(Previous.StartLine + 1, Previous.EndLine - Previous.StartLine);
                    }
        
                    // insert lines from [previous].start to [next].start
                    if (Next.StartLine > Previous.StartLine)
                    {
                        InsertLines(Previous.StartLine + 1, Next.StartLine - Previous.StartLine);
                    }
        
                    RecolorLines(Previous.StartLine, Next.StartLine);
                } 
                else 
                {        
                    // previous was a single point
                    if (Next.StartLine > Previous.StartLine)
                    {
                        // inserted data from [previous] to [next]
                        InsertLines(Previous.StartLine + 1, linecount);
                        RecolorLines(Previous.StartLine, Next.StartLine);
                    } 
                    else if (Next.StartLine < Previous.StartLine)
                    {
                        // deleted data from [next] to [previous]
                        RemoveLines(Next.StartLine + 1, -linecount);
                        RecolorLines(Next.StartLine, Previous.StartLine);
                    } 
                    else if (InnerBox.Lines.Length < Info.Length)
                    {
                        // deleted line(s) following [next]
                        RemoveLines(Previous.StartLine + 1, -linecount);
                        RecolorLines(Previous.StartLine, Previous.StartLine);
                    } 
                    else
                    {
                        // standard typing, recolour current line
                        RecolorLines(Previous.StartLine, Previous.StartLine);
                    }
                }
      
                // re-indent?
                if (redentPending)
                {
                    //' TODO: this is excessive, only need to re-indent a subset of lines
                    Redent(0);
                    redentPending = false;
                }
      
                // propogate the event
                if (TextChanged != null) TextChanged(sender, e);
            }
        }

    }
}
